// SPDX-FileCopyrightText: The openTCS Authors
// SPDX-License-Identifier: MIT
package org.opentcs.guing.common.components.properties.panel;

import static java.util.Objects.requireNonNull;

import jakarta.inject.Inject;
import java.awt.Dimension;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collections;
import java.util.List;
import java.util.Set;
import java.util.stream.Collectors;
import javax.swing.JDialog;
import javax.swing.JPanel;
import javax.swing.RowSorter;
import javax.swing.SortOrder;
import javax.swing.table.AbstractTableModel;
import javax.swing.table.TableColumn;
import javax.swing.table.TableRowSorter;
import org.opentcs.data.model.Couple;
import org.opentcs.guing.base.components.properties.type.EnvelopesProperty;
import org.opentcs.guing.base.components.properties.type.Property;
import org.opentcs.guing.base.model.EnvelopeModel;
import org.opentcs.guing.common.components.dialogs.DetailsDialogContent;
import org.opentcs.guing.common.components.dialogs.StandardContentDialog;
import org.opentcs.guing.common.persistence.ModelManager;
import org.opentcs.guing.common.util.I18nPlantOverview;
import org.opentcs.thirdparty.guing.common.jhotdraw.util.ResourceBundleUtil;
import org.opentcs.util.gui.StringTableCellRenderer;

/**
 * User interface to edit the envelope property of points and paths.
 */
public class EnvelopesPropertyEditorPanel
    extends
      JPanel
    implements
      DetailsDialogContent {

  private static final EnvelopeModel DEFAULT_ENVELOPE
      = new EnvelopeModel(
          EnvelopePanel.DEFAULT_ENVELOPE_KEY,
          List.of(
              new Couple(0, 0),
              new Couple(0, 0),
              new Couple(0, 0),
              new Couple(0, 0)
          )
      );
  /**
   * The bundle to be used.
   */
  private final ResourceBundleUtil bundle
      = ResourceBundleUtil.getBundle(I18nPlantOverview.PROPERTIES_PATH);
  /**
   * Manager of the system model.
   */
  private final ModelManager modelManager;
  /**
   * The property to edit.
   */
  private EnvelopesProperty fProperty;
  /**
   * The sorter for the table.
   */
  private TableRowSorter<EnvelopeTableModel> sorter;

  /**
   * Creates a new instance.
   *
   * @param modelManager Manages the system model.
   */
  @Inject
  @SuppressWarnings("this-escape")
  public EnvelopesPropertyEditorPanel(ModelManager modelManager) {
    this.modelManager = requireNonNull(modelManager, "modelManager");

    initComponents();
    initTable();

    setPreferredSize(new Dimension(500, 250));
  }

  @Override
  public void setProperty(Property property) {
    fProperty = (EnvelopesProperty) property;
    getTableModel().setValues(fProperty.getValue());
  }

  @Override
  public void updateValues() {
    fProperty.setValue(getTableModel().getValues());
  }

  @Override
  public Property getProperty() {
    return fProperty;
  }

  @Override
  public String getTitle() {
    return bundle.getString("envelopesPropertyEditorPanel.title");
  }

  private void initTable() {
    TableColumn columnKey = envelopesTable.getColumnModel()
        .getColumn(envelopesTable.convertColumnIndexToView(EnvelopeTableModel.COLUMN_COORDINATES));
    columnKey.setCellRenderer(new CoupleListCellRenderer());

    sorter = new TableRowSorter<>(getTableModel());
    // Sort the table by envelope keys.
    sorter.setSortKeys(
        Arrays.asList(
            new RowSorter.SortKey(EnvelopeTableModel.COLUMN_KEY, SortOrder.ASCENDING)
        )
    );
    // ...but prevent manual sorting.
    for (int i = 0; i < envelopesTable.getColumnCount(); i++) {
      sorter.setSortable(i, false);
    }
    sorter.setSortsOnUpdates(true);
    envelopesTable.setRowSorter(sorter);
  }

  private EnvelopeTableModel getTableModel() {
    return (EnvelopeTableModel) envelopesTable.getModel();
  }

  private Set<String> definedEnvelopeKeys() {
    return getTableModel().getValues().stream()
        .map(EnvelopeModel::getKey)
        .collect(Collectors.toSet());
  }

  // FORMATTER:OFF
  // CHECKSTYLE:OFF
  /**
   * This method is called from within the constructor to
   * initialize the form.
   * WARNING: Do NOT modify this code. The content of this method is
   * always regenerated by the Form Editor.
   */
  // <editor-fold defaultstate="collapsed" desc="Generated Code">//GEN-BEGIN:initComponents
  private void initComponents() {
    java.awt.GridBagConstraints gridBagConstraints;

    envelopesScrollPane = new javax.swing.JScrollPane();
    envelopesTable = new javax.swing.JTable();
    controlPanel = new javax.swing.JPanel();
    addButton = new javax.swing.JButton();
    editButton = new javax.swing.JButton();
    removeButton = new javax.swing.JButton();
    controlFiller = new javax.swing.Box.Filler(new java.awt.Dimension(0, 0), new java.awt.Dimension(0, 0), new java.awt.Dimension(32767, 32767));

    setLayout(new java.awt.BorderLayout());

    envelopesTable.setModel(new EnvelopeTableModel());
    envelopesScrollPane.setViewportView(envelopesTable);

    add(envelopesScrollPane, java.awt.BorderLayout.CENTER);

    controlPanel.setLayout(new java.awt.GridBagLayout());

    java.util.ResourceBundle bundle = java.util.ResourceBundle.getBundle("i18n/org/opentcs/plantoverview/panels/propertyEditing"); // NOI18N
    addButton.setText(bundle.getString("envelopesPropertyEditorPanel.button_add.text")); // NOI18N
    addButton.addActionListener(new java.awt.event.ActionListener() {
      public void actionPerformed(java.awt.event.ActionEvent evt) {
        addButtonActionPerformed(evt);
      }
    });
    gridBagConstraints = new java.awt.GridBagConstraints();
    gridBagConstraints.fill = java.awt.GridBagConstraints.HORIZONTAL;
    gridBagConstraints.insets = new java.awt.Insets(0, 15, 0, 0);
    controlPanel.add(addButton, gridBagConstraints);

    editButton.setText(bundle.getString("envelopesPropertyEditorPanel.button_edit.text")); // NOI18N
    editButton.addActionListener(new java.awt.event.ActionListener() {
      public void actionPerformed(java.awt.event.ActionEvent evt) {
        editButtonActionPerformed(evt);
      }
    });
    gridBagConstraints = new java.awt.GridBagConstraints();
    gridBagConstraints.gridx = 0;
    gridBagConstraints.gridy = 1;
    gridBagConstraints.fill = java.awt.GridBagConstraints.HORIZONTAL;
    gridBagConstraints.insets = new java.awt.Insets(10, 15, 10, 0);
    controlPanel.add(editButton, gridBagConstraints);

    removeButton.setText(bundle.getString("envelopesPropertyEditorPanel.button_remove.text")); // NOI18N
    removeButton.addActionListener(new java.awt.event.ActionListener() {
      public void actionPerformed(java.awt.event.ActionEvent evt) {
        removeButtonActionPerformed(evt);
      }
    });
    gridBagConstraints = new java.awt.GridBagConstraints();
    gridBagConstraints.gridx = 0;
    gridBagConstraints.gridy = 2;
    gridBagConstraints.fill = java.awt.GridBagConstraints.HORIZONTAL;
    gridBagConstraints.insets = new java.awt.Insets(0, 15, 0, 0);
    controlPanel.add(removeButton, gridBagConstraints);
    gridBagConstraints = new java.awt.GridBagConstraints();
    gridBagConstraints.gridx = 0;
    gridBagConstraints.gridy = 3;
    gridBagConstraints.fill = java.awt.GridBagConstraints.BOTH;
    gridBagConstraints.weighty = 1.0;
    controlPanel.add(controlFiller, gridBagConstraints);

    add(controlPanel, java.awt.BorderLayout.EAST);
  }// </editor-fold>//GEN-END:initComponents
  // CHECKSTYLE:ON
  // FORMATTER:ON

  private void addButtonActionPerformed(java.awt.event.ActionEvent evt) {//GEN-FIRST:event_addButtonActionPerformed
    JDialog parent = (JDialog) getTopLevelAncestor();
    EnvelopePanel content = new EnvelopePanel(
        DEFAULT_ENVELOPE,
        EnvelopePanel.Mode.CREATE,
        definedEnvelopeKeys(),
        modelManager.getModel()
    );
    StandardContentDialog dialog = new StandardContentDialog(parent, content);
    content.addInputValidationListener(dialog);
    dialog.setLocationRelativeTo(parent);
    dialog.setVisible(true);
    if (dialog.getReturnStatus() == StandardContentDialog.RET_OK
        && content.getEnvelopeModel().isPresent()) {
      getTableModel().add(content.getEnvelopeModel().get());
    }
  }//GEN-LAST:event_addButtonActionPerformed

  private void editButtonActionPerformed(java.awt.event.ActionEvent evt) {//GEN-FIRST:event_editButtonActionPerformed
    int selectedRow = envelopesTable.getSelectedRow();
    if (selectedRow == -1) {
      return;
    }
    EnvelopeModel selectedModel = getTableModel().getValueAt(
        envelopesTable.convertRowIndexToModel(selectedRow)
    );

    JDialog parent = (JDialog) getTopLevelAncestor();
    EnvelopePanel content = new EnvelopePanel(
        selectedModel,
        EnvelopePanel.Mode.EDIT,
        definedEnvelopeKeys(),
        modelManager.getModel()
    );
    StandardContentDialog dialog = new StandardContentDialog(parent, content);
    content.addInputValidationListener(dialog);
    dialog.setLocationRelativeTo(parent);
    dialog.setVisible(true);

    if (dialog.getReturnStatus() == StandardContentDialog.RET_OK
        && content.getEnvelopeModel().isPresent()) {
      getTableModel().update(
          envelopesTable.convertRowIndexToModel(selectedRow),
          content.getEnvelopeModel().get()
      );
    }
  }//GEN-LAST:event_editButtonActionPerformed

  private void removeButtonActionPerformed(java.awt.event.ActionEvent evt) {//GEN-FIRST:event_removeButtonActionPerformed
    int selectedRow = envelopesTable.getSelectedRow();
    if (selectedRow == -1) {
      return;
    }

    getTableModel().remove(envelopesTable.convertRowIndexToModel(selectedRow));
  }//GEN-LAST:event_removeButtonActionPerformed

  // FORMATTER:OFF
  // CHECKSTYLE:OFF
  // Variables declaration - do not modify//GEN-BEGIN:variables
  private javax.swing.JButton addButton;
  private javax.swing.Box.Filler controlFiller;
  private javax.swing.JPanel controlPanel;
  private javax.swing.JButton editButton;
  private javax.swing.JScrollPane envelopesScrollPane;
  private javax.swing.JTable envelopesTable;
  private javax.swing.JButton removeButton;
  // End of variables declaration//GEN-END:variables
  // CHECKSTYLE:ON
  // FORMATTER:ON

  private class EnvelopeTableModel
      extends
        AbstractTableModel {

    /**
     * The number of the "Key" column.
     */
    public static final int COLUMN_KEY = 0;
    /**
     * The number of the "Coordinates" column.
     */
    public static final int COLUMN_COORDINATES = 1;
    /**
     * The column names.
     */
    private final String[] columnNames
        = new String[]{
            bundle.getString(
                "envelopesPropertyEditorPanel.table_envelopes.column_key.headerText"
            ),
            bundle.getString(
                "envelopesPropertyEditorPanel.table_envelopes.column_coordinates.headerText"
            )
        };
    /**
     * Column classes.
     */
    private final Class<?>[] columnClasses
        = new Class<?>[]{
            String.class,
            EnvelopeModel.class
        };
    /**
     * The values in this model.
     */
    private final List<EnvelopeModel> values = new ArrayList<>();

    /**
     * Creates a new instance.
     */
    EnvelopeTableModel() {
    }

    @Override
    public Class<?> getColumnClass(int columnIndex) {
      return columnClasses[columnIndex];
    }

    @Override
    public String getColumnName(int columnIndex) {
      return columnNames[columnIndex];
    }

    @Override
    public boolean isCellEditable(int row, int column) {
      return false;
    }

    @Override
    public int getRowCount() {
      return values.size();
    }

    @Override
    public int getColumnCount() {
      return columnNames.length;
    }

    @Override
    public Object getValueAt(int rowIndex, int columnIndex) {
      if (rowIndex < 0 || rowIndex >= getRowCount()) {
        return null;
      }

      EnvelopeModel entry = values.get(rowIndex);
      switch (columnIndex) {
        case COLUMN_KEY:
          return entry.getKey();
        case COLUMN_COORDINATES:
          return entry.getVertices();
        default:
          throw new IllegalArgumentException("Invalid column index: " + columnIndex);
      }
    }

    public void setValues(List<EnvelopeModel> values) {
      requireNonNull(values, "values");

      this.values.clear();
      this.values.addAll(values);
      fireTableDataChanged();
    }

    public List<EnvelopeModel> getValues() {
      return Collections.unmodifiableList(values);
    }

    public EnvelopeModel getValueAt(int row) {
      return values.get(row);
    }

    public boolean add(EnvelopeModel envelopeModel) {
      values.add(envelopeModel);
      fireTableRowsInserted(values.size() - 1, values.size() - 1);
      return true;
    }

    public boolean update(int row, EnvelopeModel envelopeModel) {
      if (!rowInBounds(row)) {
        return false;
      }

      values.set(row, envelopeModel);
      fireTableRowsUpdated(row, row);
      return true;
    }

    public boolean remove(int row) {
      if (!rowInBounds(row)) {
        return false;
      }

      values.remove(row);
      fireTableRowsDeleted(row, row);
      return true;
    }

    private boolean rowInBounds(int row) {
      if (values.isEmpty()) {
        return false;
      }

      return row >= 0 && row <= values.size() - 1;
    }
  }

  private class CoupleListCellRenderer
      extends
        StringTableCellRenderer<List<Couple>> {

    CoupleListCellRenderer() {
      super(
          couples -> couples.stream()
              .map(couple -> "(" + couple.getX() + "," + couple.getY() + ")")
              .collect(Collectors.joining(";"))
      );
    }
  }
}
